package com.thaiopensource.validate.nvdl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;

import org.xml.sax.Attributes;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.LocatorImpl;

import com.thaiopensource.resolver.xml.sax.SAXResolver;
import com.thaiopensource.util.Localizer;
import com.thaiopensource.util.PropertyId;
import com.thaiopensource.util.PropertyMap;
import com.thaiopensource.util.PropertyMapBuilder;
import com.thaiopensource.util.Uri;
import com.thaiopensource.validate.AbstractSchema;
import com.thaiopensource.validate.IncorrectSchemaException;
import com.thaiopensource.validate.Option;
import com.thaiopensource.validate.OptionArgumentException;
import com.thaiopensource.validate.OptionArgumentPresenceException;
import com.thaiopensource.validate.ResolverFactory;
import com.thaiopensource.validate.Schema;
import com.thaiopensource.validate.ValidateProperty;
import com.thaiopensource.validate.Validator;
import com.thaiopensource.validate.auto.SchemaFuture;
import com.thaiopensource.validate.prop.wrap.WrapProperty;
import com.thaiopensource.xml.sax.CountingErrorHandler;
import com.thaiopensource.xml.sax.DelegatingContentHandler;
import com.thaiopensource.xml.sax.XmlBaseHandler;
import com.thaiopensource.xml.util.WellKnownNamespaces;

/**
 * Schema implementation for NVDL scripts.
 */
class SchemaImpl extends AbstractSchema
{
  /**
   * Mode name used when the script does not define modes and just enters
   * namespace and anyNamespace mappings directly inside rules.
   */
  static private final String IMPLICIT_MODE_NAME = "#implicit";

  /**
   * Mode name used when we have to use this script as an attributes schema. The
   * wrapper mode allows elements from any namespace.
   */
  static private final String WRAPPER_MODE_NAME = "#wrapper";

  /**
   * The NVDL URI.
   */
  static final String NVDL_URI = "http://purl.oclc.org/dsdl/nvdl/ns/structure/1.0";

  /**
   * A hash with the modes.
   */
  private final Hashtable <String, Mode> modeMap = new Hashtable <String, Mode> ();

  /**
   * A hash with the triggers on namespace. Element names are stored
   * concatenated in a string, each name preceded by #.
   */
  private final List <Trigger> triggers = new ArrayList <Trigger> ();

  /**
   * The start mode.
   */
  private Mode startMode;

  /**
   * Default base mode, rejects everything.
   */
  private final Mode defaultBaseMode;

  /**
   * Flag indicating if the schema needs to be changed to handle attributes
   * only, the element in this case is a placeholder.
   */
  private final boolean attributesSchema;

  /**
   * Wrapps an IOException as a RuntimeException.
   */
  static private final class WrappedIOException extends RuntimeException
  {
    /**
     * The actual IO Exception.
     */
    private final IOException exception;

    /**
     * Creates a wrapped exception.
     * 
     * @param exception
     *        The IOException.
     */
    private WrappedIOException (final IOException exception)
    {
      this.exception = exception;
    }

    /**
     * Get the actual IO Exception.
     * 
     * @return IOException.
     */
    private IOException getException ()
    {
      return exception;
    }
  }

  /**
   * Stores information about options that must be supported by the validator.
   */
  static private class MustSupportOption
  {
    /**
     * The option name.
     */
    private final String name;
    /**
     * The property id.
     */
    private final PropertyId pid;

    /**
     * Locator pointing to where this option is declared.
     */
    private final Locator locator;

    /**
     * Creates a must support option.
     * 
     * @param name
     *        The option name
     * @param pid
     *        property id.
     * @param locator
     *        locator pointing to where this option is declared.
     */
    MustSupportOption (final String name, final PropertyId pid, final Locator locator)
    {
      this.name = name;
      this.pid = pid;
      this.locator = locator;
    }
  }

  /**
   * This class is registered as content handler on the XMLReader that parses
   * the NVDL script. It creates the Schema representation for this script and
   * also validates the script against the NVDL schema.
   */
  private class Handler extends DelegatingContentHandler implements SchemaFuture
  {
    /**
     * The schema receiver. Used to cretae other schemas and access options.
     */
    private final SchemaReceiverImpl sr;

    /**
     * Flag indicating that we encountered an error.
     */
    private boolean hadError = false;

    /**
     * The error handler.
     */
    private final ErrorHandler eh;

    /**
     * A counting error handler that wraps the error handler. It is useful to
     * stop early if we encounter errors.
     */
    private final CountingErrorHandler ceh;

    /**
     * The Resolver to use for resolving URIs and entities.
     */
    private final SAXResolver resolver;

    /**
     * Convert error keys to messages.
     */
    private final Localizer localizer = new Localizer (SchemaImpl.class);

    /**
     * Error locator.
     */
    private Locator locator;

    /**
     * Handle xml:base attributes.
     */
    private final XmlBaseHandler xmlBaseHandler = new XmlBaseHandler ();

    /**
     * For ignoring foreign elements.
     */
    private int foreignDepth = 0;

    /**
     * The value of rules/@schemaType
     */
    private String defaultSchemaType;

    /**
     * The validator that checks the script against the NVDL RelaxNG schema.
     */
    private Validator validator;

    /**
     * Stores mode data. We use this to handle included and nested modes.
     */
    class ModeData
    {
      /**
       * Points to the current mode.
       */
      private Mode currentMode = null;

      /**
       * The value of the match attribute on the current rule.
       */
      private ElementsOrAttributes match;

      /**
       * The current element actions.
       */
      private ActionSet actions;

      /**
       * The current attribute actions.
       */
      private AttributeActionSet attributeActions;

      /**
       * The URI reference for the schema for the current validate action. This
       * is as specified in the attribute before any resolution.
       */
      private String schemaUriRef;

      /**
       * The base URI to be used for resolving schemaUriRef.
       */
      private String schemaUriBase;

      /**
       * The current validate action schema type.
       */
      private String schemaType;

      /**
       * The options defined for a validate action.
       */
      private PropertyMapBuilder options;

      /**
       * The options that must be supported by the validator for the current
       * validate action.
       */
      private final Vector <MustSupportOption> mustSupportOptions = new Vector <MustSupportOption> ();

      /**
       * The current mode usage, for the current action.
       */
      private ModeUsage modeUsage;

      /**
       * Flag indicating if we are in a namespace rule or in an anyNamespace
       * rule.
       */
      private boolean anyNamespace;

      /**
       * The lastMode stores the last created mode. For example when we have an
       * action we need to create the ModeUsage for it and lastMode points to
       * the mode for that action. It is possible that lastMode is created
       * without having encountered its definition, in the case of nested modes.
       * In that case we have no useMode attribute but a mode element can appear
       * further inside the action (a nested mode). If no mode appears inside
       * the action then we need to resolve the anonymous mode that is not
       * defined to the current mode.
       */
      private Mode lastMode;
    }

    /**
     * Stores mode data.
     */
    ModeData md = new ModeData ();

    /**
     * Keeps the mode data stack.
     */
    private final Stack <ModeData> modeDataStack = new Stack <ModeData> ();

    /**
     * Keeps the elements from NVDL representing the current context. We need it
     * to distinguish between modes, included modes and nested modes.
     */
    private final Stack <String> nvdlStack = new Stack <String> ();

    /**
     * Creates a handler.
     * 
     * @param sr
     *        The Schema Receiver implementation for NVDL schemas.
     */
    Handler (final SchemaReceiverImpl sr)
    {
      this.sr = sr;
      this.eh = sr.getProperties ().get (ValidateProperty.ERROR_HANDLER);
      this.ceh = new CountingErrorHandler (this.eh);
      this.resolver = ResolverFactory.createResolver (sr.getProperties ());
    }

    /**
     * Callback with the document locator.
     * 
     * @param locator
     *        The document locator.
     */
    @Override
    public void setDocumentLocator (final Locator locator)
    {
      xmlBaseHandler.setLocator (locator);
      this.locator = locator;
    }

    /**
     * On start document.
     */
    @Override
    public void startDocument () throws SAXException
    {
      // creates a validator that validates against the schema for NVDL.
      try
      {
        final PropertyMapBuilder builder = new PropertyMapBuilder (sr.getProperties ());
        builder.put (ValidateProperty.ERROR_HANDLER, ceh);
        validator = sr.getNvdlSchema ().createValidator (builder.toPropertyMap ());
      }
      catch (final IOException e)
      {
        throw new WrappedIOException (e);
      }
      catch (final IncorrectSchemaException e)
      {
        throw new RuntimeException ("internal error in RNG schema for NVDL");
      }
      // set that validator content handler as delegate to receive the NVDL
      // schema content.
      setDelegate (validator.getContentHandler ());
      // forward the setDocumentLocator and startDocument to the delegate
      // handler.
      if (locator != null)
        super.setDocumentLocator (locator);
      super.startDocument ();
    }

    public Schema getSchema () throws IncorrectSchemaException, SAXException
    {
      if (validator == null || ceh.getHadErrorOrFatalError ())
        throw new IncorrectSchemaException ();
      final Hashset openModes = new Hashset ();
      final Hashset checkedModes = new Hashset ();
      for (final Enumeration <String> e = modeMap.keys (); e.hasMoreElements ();)
      {
        final String modeName = e.nextElement ();
        final Mode mode = modeMap.get (modeName);
        if (!mode.isDefined ())
          error ("undefined_mode", modeName, mode.getWhereUsed ());
        for (Mode tem = mode; tem != null; tem = tem.getBaseMode ())
        {
          if (checkedModes.contains (tem))
            break;
          if (openModes.contains (tem))
          {
            error ("mode_cycle", tem.getName (), tem.getWhereDefined ());
            break;
          }
          openModes.add (tem);
        }
        checkedModes.addAll (openModes);
        openModes.clear ();
      }
      if (hadError)
        throw new IncorrectSchemaException ();
      return SchemaImpl.this;
    }

    public RuntimeException unwrapException (final RuntimeException e) throws SAXException,
                                                                      IOException,
                                                                      IncorrectSchemaException
    {
      if (e instanceof WrappedIOException)
        throw ((WrappedIOException) e).getException ();
      return e;
    }

    /**
     * Start element callback.
     * 
     * @param uri
     *        The namespace uri for this element.
     * @param localName
     *        The element local name.
     * @param qName
     *        The element qualified name.
     * @param attributes
     *        The attributes of this element.
     */
    @Override
    public void startElement (final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException
    {
      // call delegate handler
      super.startElement (uri, localName, qName, attributes);
      // handle xml:base
      xmlBaseHandler.startElement ();
      final String xmlBase = attributes.getValue (WellKnownNamespaces.XML, "base");
      if (xmlBase != null)
        xmlBaseHandler.xmlBaseAttribute (xmlBase);
      // ignore foreign elements
      if (!NVDL_URI.equals (uri) || foreignDepth > 0)
      {
        foreignDepth++;
        return;
      }
      // stop if we got errors.
      if (ceh.getHadErrorOrFatalError ())
        return;
      // dispatch based on the element name
      if (localName.equals ("rules"))
        parseRules (attributes);
      else
        if (localName.equals ("mode"))
        {
          final String parent = nvdlStack.peek ();
          if ("rules".equals (parent))
            parseMode (attributes);
          else
            if ("mode".equals (parent))
              // mode inside mode - included mode.
              parseIncludedMode (attributes);
            else
              // nested mode
              parseNestedMode (attributes);
        }
        else
          if (localName.equals ("namespace"))
            parseNamespace (attributes);
          else
            if (localName.equals ("anyNamespace"))
              parseAnyNamespace (attributes);
            else
              if (localName.equals ("validate"))
                parseValidate (attributes);
              else
                if (localName.equals ("reject"))
                  parseReject (attributes);
                else
                  if (localName.equals ("attach"))
                    parseAttach (attributes);
                  else
                    if (localName.equals ("unwrap"))
                      parseUnwrap (attributes);
                    else
                      if (localName.equals ("attachPlaceholder"))
                        parseAttachPlaceholder (attributes);
                      else
                        if (localName.equals ("allow"))
                          parseAllow (attributes);
                        else
                          if (localName.equals ("context"))
                            parseContext (attributes);
                          else
                            if (localName.equals ("option"))
                              parseOption (attributes);
                            else
                              if (localName.equals ("trigger"))
                                parseTrigger (attributes);
                              else
                                if (localName.equals ("schema"))
                                  error ("embedded_schemas");
                                else
                                  if (localName.equals ("cancelNestedActions"))
                                    parseCancelNestedActions (attributes);
                                  else
                                    if (localName.equals ("message"))
                                    {} // noop
                                    else
                                      throw new RuntimeException ("unexpected element \"" + localName + "\"");
      // add the NVDL element on the stack
      nvdlStack.push (localName);

    }

    /**
     * End element callback.
     * 
     * @param namespaceURI
     *        The namespace uri for this element.
     * @param localName
     *        The element local name.
     * @param qName
     *        The element qualified name.
     */
    @Override
    public void endElement (final String namespaceURI, final String localName, final String qName) throws SAXException
    {
      // call the delegate handler
      super.endElement (namespaceURI, localName, qName);
      // handle xml:base
      xmlBaseHandler.endElement ();
      // ignore foreign elements
      if (foreignDepth > 0)
      {
        foreignDepth--;
        return;
      }
      // exit early if we got errors.
      if (ceh.getHadErrorOrFatalError ())
        return;
      // pop the NVDL element from the stack
      nvdlStack.pop ();
      // dispatch based on element name.
      if (localName.equals ("validate"))
        finishValidate ();
      else
        if (localName.equals ("mode"))
        {
          final String parent = nvdlStack.peek ();
          if ("rules".equals (parent))
            finishMode ();
          else
            if ("mode".equals (parent))
              // mode inside mode - included mode.
              finishIncludedMode ();
            else
              // nested mode.
              finishNestedMode ();
        }
    }

    /**
     * Parse the rules element. Initializes the start mode the current mode the
     * defaultSchemaType
     * 
     * @param attributes
     *        The rule element attributes.
     */
    private void parseRules (final Attributes attributes)
    {
      startMode = getModeAttribute (attributes, "startMode");
      // If not start mode specified we create an implicit mode.
      if (startMode == null)
      {
        startMode = lookupCreateMode (IMPLICIT_MODE_NAME);
        md.currentMode = startMode;
        // mark this implicit mode as not defined in the schema.
        startMode.noteDefined (null);
      }
      // Set the current location as the location the start mode is first used.
      startMode.noteUsed (locator);
      // if the schema should be used for validating only attributes
      // we need to create a wrapper that allows any element from any namespace
      // as the placeholder for the attributes we want to validate.
      if (attributesSchema)
      {
        final Mode wrapper = lookupCreateMode (WRAPPER_MODE_NAME);
        // creates element actions - allow and set them for any namespace
        // the attributes will be validated further in the real schema start
        // mode.
        final ActionSet actions = new ActionSet ();
        actions.addNoResultAction (new AllowAction (new ModeUsage (startMode, startMode)));
        wrapper.bindElement (NamespaceSpecification.ANY_NAMESPACE, NamespaceSpecification.DEFAULT_WILDCARD, actions);
        wrapper.noteDefined (null);
        // we use the wrapper mode as the start mode.
        startMode = wrapper;
      }
      // Get the default value for schema type if it is specified in
      // rule/@schemaType.
      defaultSchemaType = getSchemaType (attributes);
    }

    /**
     * Parse a mode element.
     * 
     * @param attributes
     *        The element attributes.
     * @throws SAXException
     */
    private void parseMode (final Attributes attributes) throws SAXException
    {
      // Get the mode (create it if it does not exists) corresponding to the
      // name attribute.
      md.currentMode = getModeAttribute (attributes, "name");
      // If already defined, report errors.
      if (md.currentMode.isDefined ())
      {
        error ("duplicate_mode", md.currentMode.getName ());
        error ("first_mode", md.currentMode.getName (), md.currentMode.getWhereDefined ());
      }
      else
      {
        // Check if we have a base mode and set that as the base mode for this
        // mode.
        final Mode base = getModeAttribute (attributes, "extends");
        if (base != null)
          md.currentMode.setBaseMode (base);
        // record the location where this mode is defined.
        md.currentMode.noteDefined (locator);
      }
    }

    /**
     * Parse a mode element.
     * 
     * @param attributes
     *        The element attributes.
     * @throws SAXException
     */
    private void parseIncludedMode (final Attributes attributes) throws SAXException
    {
      // Create an anonymous mode.
      final Mode parent = md.currentMode;
      modeDataStack.push (md);
      md = new ModeData ();
      md.currentMode = new Mode (defaultBaseMode);
      md.currentMode.noteDefined (locator);
      parent.addIncludedMode (md.currentMode);
    }

    /**
     * Parse a mode element.
     * 
     * @param attributes
     *        The element attributes.
     * @throws SAXException
     */
    private void parseNestedMode (final Attributes attributes) throws SAXException
    {
      // Nested mode is an anonymous mode inside an action. The action does
      // not have a useMode attribute and we alrady have the mode for that
      // created in the current mode data lastMode, so we use that and define it
      // as this nested mode.
      final ModeData oldMd = md;
      modeDataStack.push (md);
      md = new ModeData ();
      md.currentMode = oldMd.lastMode;
      // If already defined, report errors.
      if (md.currentMode.isDefined ())
      {
        error ("duplicate_mode", md.currentMode.getName ());
        error ("first_mode", md.currentMode.getName (), md.currentMode.getWhereDefined ());
      }
      else
      {
        // record the location where this mode is defined.
        md.currentMode.noteDefined (locator);
      }
    }

    /**
     * Parse a namespace rule.
     * 
     * @param attributes
     *        The namespace element attributes.
     * @throws SAXException
     */
    private void parseNamespace (final Attributes attributes) throws SAXException
    {
      md.anyNamespace = false;
      parseRule (getNs (attributes), attributes);
    }

    /**
     * Parse an anyNamespace rule.
     * 
     * @param attributes
     *        The anyNamespace element attributes.
     * @throws SAXException
     */
    private void parseAnyNamespace (final Attributes attributes) throws SAXException
    {
      md.anyNamespace = true;
      parseRule (NamespaceSpecification.ANY_NAMESPACE, attributes);
    }

    /**
     * Parse namespace and anyNamespace rules/
     * 
     * @param ns
     *        The namespace, ##any for anyNamespace
     * @param attributes
     *        The rule attributes.
     * @throws SAXException
     */
    private void parseRule (final String ns, final Attributes attributes) throws SAXException
    {
      // gets the value of the match attribute, defaults to match elements only.
      md.match = toElementsOrAttributes (attributes.getValue ("", "match"), ElementsOrAttributes.ELEMENTS);
      String wildcard = attributes.getValue ("", "wildCard");
      if (wildcard == null)
      {
        wildcard = NamespaceSpecification.DEFAULT_WILDCARD;
      }

      // check if match attributes
      if (md.match.containsAttributes ())
      {
        // creates an empty attributes action set.
        md.attributeActions = new AttributeActionSet ();
        // if we already have attribute actions for this namespace
        // signal an error.
        if (!md.currentMode.bindAttribute (ns, wildcard, md.attributeActions))
        {
          if (ns.equals (NamespaceSpecification.ANY_NAMESPACE))
            error ("duplicate_attribute_action_any_namespace");
          else
            error ("duplicate_attribute_action", ns);
        }
      }
      else
        md.attributeActions = null;
      // XXX: george // } else md.attributeActions=null; //???

      // check if match elements
      if (md.match.containsElements ())
      {
        // creates an empty action set.
        md.actions = new ActionSet ();
        // if we already have actions for this namespace
        // signal an error.
        if (!md.currentMode.bindElement (ns, wildcard, md.actions))
        {
          if (ns.equals (NamespaceSpecification.ANY_NAMESPACE))
            error ("duplicate_element_action_any_namespace");
          else
            error ("duplicate_element_action", ns);
        }
      }
      else
        md.actions = null;
    }

    /**
     * Parse a validate action.
     * 
     * @param attributes
     *        The validate element attributes.
     * @throws SAXException
     */
    private void parseValidate (final Attributes attributes) throws SAXException
    {
      // get the resolved URI pointing to the schema.
      md.schemaUriRef = getSchema (attributes);
      md.schemaUriBase = xmlBaseHandler.getBaseUri ();
      // get the schema type
      md.schemaType = getSchemaType (attributes);
      // if no schemaType attribute, use the default schema type.
      if (md.schemaType == null)
        md.schemaType = defaultSchemaType;
      if (SchemaReceiverImpl.LEGACY_RNC_MEDIA_TYPE.equals (md.schemaType))
        warning ("legacy_rnc_media_type", locator);
      // if we matched on elements create a mode usage.
      if (md.actions != null)
        md.modeUsage = getModeUsage (attributes);
      else
        md.modeUsage = null;
      // prepare to receive validate options.
      md.options = new PropertyMapBuilder ();
      md.mustSupportOptions.clear ();
    }

    /**
     * Notification that the validate element ends.
     * 
     * @throws SAXException
     */
    private void finishValidate () throws SAXException
    {
      if (md.schemaUriRef != null)
      {
        try
        {
          // if we had attribute actions, that is matching attributes
          // we add a schema to the attributes action set.
          if (md.attributeActions != null)
          {
            final Schema schema = createSubSchema (true);
            md.attributeActions.addSchema (schema);
          }
          // if we had element actions, that is macting elements
          // we add a validate action with the schema and the specific mode
          // usage.
          if (md.actions != null)
          {
            final Schema schema = createSubSchema (false);
            md.actions.addNoResultAction (new ValidateAction (md.modeUsage, schema));
          }
        }
        catch (final IncorrectSchemaException e)
        {
          hadError = true;
        }
        catch (final IOException e)
        {
          throw new WrappedIOException (e);
        }
      }
    }

    /**
     * Notification that the mode element ends.
     * 
     * @throws SAXException
     */
    private void finishMode () throws SAXException
    {}

    /**
     * Notification that the mode element ends.
     * 
     * @throws SAXException
     */
    private void finishIncludedMode () throws SAXException
    {
      md = modeDataStack.pop ();
    }

    /**
     * Notification that the mode element ends.
     * 
     * @throws SAXException
     */
    private void finishNestedMode () throws SAXException
    {
      md = modeDataStack.pop ();
    }

    /**
     * Creates a sub schema for the ending validate action (this is called from
     * finishValidate).
     * 
     * @param isAttributesSchema
     *        If the schema is intended to validate only attributes.
     * @return A Schema.
     * @throws IOException
     * @throws IncorrectSchemaException
     * @throws SAXException
     */
    private Schema createSubSchema (final boolean isAttributesSchema) throws IOException,
                                                                     IncorrectSchemaException,
                                                                     SAXException
    {
      // the user specified options
      final PropertyMap requestedProperties = md.options.toPropertyMap ();
      // let the schema receiver create a child schema
      // XXX parse the media type to the resolver
      final Schema schema = sr.createChildSchema (resolver.resolve (md.schemaUriRef, md.schemaUriBase),
                                                  md.schemaType,
                                                  requestedProperties,
                                                  isAttributesSchema);
      // get the schema properties
      final PropertyMap actualProperties = schema.getProperties ();
      // Check if the actual properties match the must support properties.
      for (MustSupportOption mso : md.mustSupportOptions)
      {
        final Object actualValue = actualProperties.get (mso.pid);
        if (actualValue == null)
          error ("unsupported_option", mso.name, mso.locator);
        else
          if (!actualValue.equals (requestedProperties.get (mso.pid)))
            error ("unsupported_option_arg", mso.name, mso.locator);
      }
      return schema;
    }

    /**
     * Parse a validate option.
     * 
     * @param attributes
     *        The option element attributes.
     * @throws SAXException
     */
    private void parseOption (final Attributes attributes) throws SAXException
    {
      // get the mustSupport flag
      boolean mustSupport;
      String mustSupportValue = attributes.getValue ("", "mustSupport");
      if (mustSupportValue != null)
      {
        mustSupportValue = mustSupportValue.trim ();
        mustSupport = mustSupportValue.equals ("1") || mustSupportValue.equals ("true");
      }
      else
        mustSupport = false;
      // Resolve the option if specified relative to the NVDL URI.
      final String name = Uri.resolve (NVDL_URI, attributes.getValue ("", "name"));
      final Option option = sr.getOption (name);
      // check if we got a known option.
      if (option == null)
      {
        if (mustSupport)
          error ("unknown_option", name);
      }
      else
      {
        // known option, look for arguments
        final String arg = attributes.getValue ("", "arg");
        try
        {
          final PropertyId pid = option.getPropertyId ();
          Object value = option.valueOf (arg);
          final Object oldValue = md.options.get (pid);
          if (oldValue != null)
          {
            value = option.combine (new Object [] { oldValue, value });
            if (value == null)
              error ("duplicate_option", name);
            else
              md.options.put (pid, value);
          }
          else
          {
            md.options.put (pid, value);
            md.mustSupportOptions.addElement (new MustSupportOption (name,
                                                                     pid,
                                                                     locator == null ? null : new LocatorImpl (locator)));
          }
        }
        catch (final OptionArgumentPresenceException e)
        {
          error (arg == null ? "option_requires_argument" : "option_unexpected_argument", name);
        }
        catch (final OptionArgumentException e)
        {
          if (arg == null)
            error ("option_requires_argument", name);
          else
            error ("option_bad_argument", name, arg);
        }
      }
    }

    /**
     * Parse a trigger element.
     * 
     * @param attributes
     *        The trigger element attributes.
     * @throws SAXException
     */
    private void parseTrigger (final Attributes attributes) throws SAXException
    {
      // get the ns and nameList, we know they are not null as we validate
      // against the nvdl.rng schema.
      final String ns = attributes.getValue ("", "ns");
      final String nameList = attributes.getValue ("", "nameList");
      final StringTokenizer st = new StringTokenizer (nameList);
      final Set <String> names = new HashSet <String> (st.countTokens ());
      while (st.hasMoreTokens ())
      {
        names.add (st.nextToken ());
      }
      triggers.add (new Trigger (ns, names));
    }

    /**
     * Parse an attach action.
     * 
     * @param attributes
     *        The attach element attributes.
     */
    private void parseAttach (final Attributes attributes)
    {
      // if the rule matched attributes set the attach flag in the attribute
      // actions.
      if (md.attributeActions != null)
        md.attributeActions.setAttach (true);
      // if the rule matched elements, the the mode usage and create a attach
      // result action
      // with that mode usage.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.setResultAction (new AttachAction (md.modeUsage));
      }
      // no element action -> no modeUsage.
      else
        md.modeUsage = null;
    }

    /**
     * Parse an unwrap action.
     * 
     * @param attributes
     *        The unwrap element attributes.
     */
    private void parseUnwrap (final Attributes attributes)
    {
      // this makes sense only on elements
      // if we have element actions, create the mode usage and add
      // an unwrap action with this mode usage.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.setResultAction (new UnwrapAction (md.modeUsage));
      }
      // no element actions, no modeUsage.
      else
        md.modeUsage = null;
    }

    /**
     * Parse an attachPlaceholder action.
     * 
     * @param attributes
     *        The attachPlaceholder element attributes.
     */
    private void parseAttachPlaceholder (final Attributes attributes)
    {
      // this makes sense only on elements
      // if we have element actions, create the mode usage and add
      // an attachPlaceholder action with this mode usage.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.setResultAction (new AttachPlaceholderAction (md.modeUsage));
      }
      // no element actions, no modeUsage.
      else
        md.modeUsage = null;
    }

    /**
     * Parse an allow action.
     * 
     * @param attributes
     *        The allow element attributes.
     */
    private void parseAllow (final Attributes attributes)
    {
      // if we match on elements, create the mode usage and add an allow action.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.addNoResultAction (new AllowAction (md.modeUsage));
      }
      // no actions, no mode usage.
      else
        md.modeUsage = null;
      // no need to add anything in the attribute actions, allow
      // is equivalent with a noop action.
    }

    /**
     * Parse a reject action.
     * 
     * @param attributes
     *        The reject element attributes.
     */
    private void parseReject (final Attributes attributes)
    {
      // if element actions, get the mode usage and add a reject
      // action with this mode usage.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.addNoResultAction (new RejectAction (md.modeUsage));
      }
      // no actions, no mode usage
      else
        md.modeUsage = null;
      // if attribute actions, set the reject flag.
      if (md.attributeActions != null)
        md.attributeActions.setReject (true);
    }

    /**
     * Parse a cancel nested actions action.
     * 
     * @param attributes
     *        The cancelNestedActions element attributes.
     */
    private void parseCancelNestedActions (final Attributes attributes)
    {
      // if we match on elements, create the mode usage and add a
      // cancelNestedActions action.
      if (md.actions != null)
      {
        md.modeUsage = getModeUsage (attributes);
        md.actions.setCancelNestedActions (true);
      }
      // no actions, no mode usage.
      else
        md.modeUsage = null;

      // if attribute actions set the cancelNestedActions flag.
      if (md.attributeActions != null)
      {
        md.attributeActions.setCancelNestedActions (true);
      }
    }

    /**
     * Parse context dependent mode usages.
     * 
     * @param attributes
     *        The context element attributes.
     * @throws SAXException
     */
    private void parseContext (final Attributes attributes) throws SAXException
    {
      // TODO: check this in the NVDL spec.
      // context not allowed within anyNamespace.???
      // IT SEEMS IT IS ALLOWED IN NVDL...
      // if (md.anyNamespace) {
      // error("context_any_namespace");
      // return;
      // }
      // Get the mode to be used further on this context.
      final Mode mode = getUseMode (attributes);
      md.lastMode = mode;
      try
      {
        // parse the path value into a list of Path objects
        // and add them to the mode usage
        final Vector paths = Path.parse (attributes.getValue ("", "path"));
        // XXX warning if modeUsage is null
        if (md.modeUsage != null)
        {
          for (int i = 0, len = paths.size (); i < len; i++)
          {
            final Path path = (Path) paths.elementAt (i);
            if (!md.modeUsage.addContext (path.isRoot (), path.getNames (), mode))
              error ("duplicate_path", path.toString ());
          }
        }
      }
      catch (final Path.ParseException e)
      {
        error (e.getMessageKey ());
      }
    }

    /**
     * Get the URI specified by a schema attribute and if we have a relative
     * location resolve that against the base URI taking into account also
     * eventual xml:base attributes.
     * 
     * @param attributes
     *        The validate element attributes.
     * @return A resolved URI as string.
     * @throws SAXException
     *         If the schema contains a fragment id.
     */
    private String getSchema (final Attributes attributes) throws SAXException
    {
      String schemaUri = attributes.getValue ("", "schema");
      if ("".equals (schemaUri))
      {
        error ("no_schema");
        schemaUri = null;
      }
      if (schemaUri != null)
      {
        if (Uri.hasFragmentId (schemaUri))
          error ("schema_fragment_id");
        return schemaUri;
      }
      return null;
    }

    /**
     * Get the schema type
     * 
     * @param attributes
     *        The attributes
     * @return The value of the schemaType attribute.
     */
    private String getSchemaType (final Attributes attributes)
    {
      return attributes.getValue ("", "schemaType");
    }

    /**
     * Get an ElementsOrAttributes instance depending on the match attribute
     * value.
     * 
     * @param value
     *        The match attribute value.
     * @param defaultValue
     *        The default value if value is null.
     * @return an ElementsOrAttributes constant.
     */
    private ElementsOrAttributes toElementsOrAttributes (final String value, final ElementsOrAttributes defaultValue)
    {
      if (value == null)
        return defaultValue;
      ElementsOrAttributes eoa = ElementsOrAttributes.NEITHER;
      if (value.indexOf ("elements") >= 0)
        eoa = eoa.addElements ();
      if (value.indexOf ("attributes") >= 0)
        eoa = eoa.addAttributes ();
      return eoa;
    }

    /**
     * Creates a mode usage that matches current mode and uses further the mode
     * specified by the useMode attribute.
     * 
     * @param attributes
     *        The action element attributes.
     * @return A mode usage from currentMode to the mode specified by the
     *         useMode attribute.
     */
    private ModeUsage getModeUsage (final Attributes attributes)
    {
      md.lastMode = getUseMode (attributes);
      return new ModeUsage (md.lastMode, md.currentMode);
    }

    /**
     * Get the Mode for the useMode attribute.
     * 
     * @param attributes
     *        the attributes
     * @return the mode with the useMode name or the special Mode.CURRENT mode
     *         that will be resolved to the current mode in a Mode usage.
     */
    private Mode getUseMode (final Attributes attributes)
    {
      final Mode mode = getModeAttribute (attributes, "useMode");
      if (mode == null)
        return new Mode (defaultBaseMode);
      mode.noteUsed (locator);
      return mode;
    }

    /**
     * Get the namespace from the ns attribute. Also check that the namespace is
     * an absolute URI and report an error otherwise.
     * 
     * @param attributes
     *        The list of attributes of the namespace element
     * @return The ns value.
     * @throws SAXException
     */
    private String getNs (final Attributes attributes) throws SAXException
    {
      final String ns = attributes.getValue ("", "ns");
      if (ns != null && !Uri.isAbsolute (ns) && !ns.equals (""))
        error ("ns_absolute");
      return ns;
    }

    /**
     * Report a no arguments error from a key.
     * 
     * @param key
     *        The error key.
     * @throws SAXException
     */
    void error (final String key) throws SAXException
    {
      hadError = true;
      if (eh == null)
        return;
      eh.error (new SAXParseException (localizer.message (key), locator));
    }

    /**
     * Report an one argument error.
     * 
     * @param key
     *        The error key.
     * @param arg
     *        The argument.
     * @throws SAXException
     */
    void error (final String key, final String arg) throws SAXException
    {
      hadError = true;
      if (eh == null)
        return;
      eh.error (new SAXParseException (localizer.message (key, arg), locator));
    }

    /**
     * Report an one argument error with location.
     * 
     * @param key
     *        The error key.
     * @param arg
     *        The argument.
     * @param locator
     *        The location.
     * @throws SAXException
     */
    void error (final String key, final String arg, final Locator locator) throws SAXException
    {
      hadError = true;
      if (eh == null)
        return;
      eh.error (new SAXParseException (localizer.message (key, arg), locator));
    }

    /**
     * Report a two arguments error.
     * 
     * @param key
     *        The error key.
     * @param arg1
     *        The first argument.
     * @param arg2
     *        The second argument.
     * @throws SAXException
     */
    void error (final String key, final String arg1, final String arg2) throws SAXException
    {
      hadError = true;
      if (eh == null)
        return;
      eh.error (new SAXParseException (localizer.message (key, arg1, arg2), locator));
    }

    /**
     * Report a no argument warning with location.
     * 
     * @param key
     *        The warning key.
     * @param locator
     *        The location.
     * @throws SAXException
     */
    void warning (final String key, final Locator locator) throws SAXException
    {
      if (eh == null)
        return;
      eh.warning (new SAXParseException (localizer.message (key), locator));
    }
  }

  /**
   * Creates a NVDL schema implementation. Initializes the attributesSchema flag
   * and the built in modes.
   * 
   * @param properties
   *        Properties.
   */
  SchemaImpl (final PropertyMap properties)
  {
    super (properties);
    this.attributesSchema = properties.contains (WrapProperty.ATTRIBUTE_OWNER);
    makeBuiltinMode ("#allow", AllowAction.class);
    makeBuiltinMode ("#attach", AttachAction.class);
    makeBuiltinMode ("#unwrap", UnwrapAction.class);
    defaultBaseMode = makeBuiltinMode ("#reject", RejectAction.class);
  }

  /**
   * Makes a built in mode.
   * 
   * @param name
   *        The mode name.
   * @param cls
   *        The action class.
   * @return A Mode object.
   */
  private Mode makeBuiltinMode (final String name, final Class cls)
  {
    // lookup/create a mode with the given name.
    final Mode mode = lookupCreateMode (name);
    // Init the element action set for this mode.
    final ActionSet actions = new ActionSet ();
    // from the current mode we will use further the built in mode.
    final ModeUsage modeUsage = new ModeUsage (Mode.CURRENT, mode);
    // Add the action corresponding to the built in mode.
    if (cls == AttachAction.class)
      actions.setResultAction (new AttachAction (modeUsage));
    else
      if (cls == AllowAction.class)
        actions.addNoResultAction (new AllowAction (modeUsage));
      else
        if (cls == UnwrapAction.class)
          actions.setResultAction (new UnwrapAction (modeUsage));
        else
          actions.addNoResultAction (new RejectAction (modeUsage));
    // set the actions on any namespace.
    mode.bindElement (NamespaceSpecification.ANY_NAMESPACE, NamespaceSpecification.DEFAULT_WILDCARD, actions);
    // the mode is not defined in the script explicitelly
    mode.noteDefined (null);
    // creates attribute actions
    final AttributeActionSet attributeActions = new AttributeActionSet ();
    // if we have a schema for attributes then in the built in modes
    // we reject attributes by default
    // otherwise we attach attributes by default in the built in modes
    if (attributesSchema)
      attributeActions.setReject (true);
    else
      attributeActions.setAttach (true);
    // set the attribute actions on any namespace
    mode.bindAttribute (NamespaceSpecification.ANY_NAMESPACE, NamespaceSpecification.DEFAULT_WILDCARD, attributeActions);
    return mode;
  }

  /**
   * Installs the schema handler on the reader.
   * 
   * @param in
   *        The reader.
   * @param sr
   *        The schema receiver.
   * @return The installed handler that implements also SchemaFuture.
   */
  SchemaFuture installHandlers (final XMLReader in, final SchemaReceiverImpl sr)
  {
    final Handler h = new Handler (sr);
    in.setContentHandler (h);
    return h;
  }

  /**
   * Creates a Validator for validating XML documents against this NVDL script.
   * 
   * @param properties
   *        properties.
   */
  public Validator createValidator (final PropertyMap properties)
  {
    return new ValidatorImpl (startMode, triggers, properties);
  }

  /**
   * Get the mode specified by an attribute from no namespace.
   * 
   * @param attributes
   *        The attributes.
   * @param localName
   *        The attribute name.
   * @return The mode refered by the licanName attribute.
   */
  private Mode getModeAttribute (final Attributes attributes, final String localName)
  {
    return lookupCreateMode (attributes.getValue ("", localName));
  }

  /**
   * Gets a mode with the given name from the mode map. If not present then it
   * creates a new mode extending the default base mode.
   * 
   * @param name
   *        The mode to look for or create if it does not exist.
   * @return Always a not null mode.
   */
  private Mode lookupCreateMode (String name)
  {
    if (name == null)
      return null;
    name = name.trim ();
    Mode mode = modeMap.get (name);
    if (mode == null)
    {
      mode = new Mode (name, defaultBaseMode);
      modeMap.put (name, mode);
    }
    return mode;
  }

}
